package com.github.xzb617.client.flow.core.statics;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 统计时间窗口
 * <p>
 *     借鉴Sentinel限流的滑动时间算法
 * </p>
 * @author xzb617
 */
public class Window {

    private final Logger LOGGER = LoggerFactory.getLogger(Window.class);

    private final Lock LOCK = new ReentrantLock();
    private final Slice[] slices;
    private final long windowsInterval;
    private final long sliceInterval;

    public Window(long windowsInterval, long spanInterval) {
        this.windowsInterval = windowsInterval;
        this.sliceInterval = spanInterval;
        int spanCount = (int) (windowsInterval / spanInterval);
        this.slices = new Slice[spanCount];
        for (int i = 0; i < spanCount; i++) {
            this.slices[i] = new Slice(0L, spanInterval);
        }
    }

    /**
     * 获取当前时间窗口的总计数
     * @return
     */
    public Slice getCurrentSlice(long currentTimestamp) {
        // 计算当前时间落在那个位置上
        int spanIdx = this.calcCurrentTimeIdx(currentTimestamp);
        long startTime = this.calcCurrentTimeStartTime(currentTimestamp);

        while (true) {
            // 计算当前时间窗的开始时间
            Slice slice = this.slices[spanIdx];
            if (slice == null) {
                slice = new Slice(currentTimestamp, this.sliceInterval);
                boolean successSet = setSliceToArray(slice, spanIdx);
                if (successSet) {
                    return slice;
                } else {
                    // 让出cpu资源，但是该cpu资源不一定会给到其它同样优先级的线程
                    // 也可能本线程继续使用
                    Thread.yield();
                }
            }

            // 刚好落在该样本时间窗内
            if (startTime == slice.getStartTime()) {
                return slice;
            }
            // 之前的样本时间已过时，重置该样本
            else if (startTime > slice.getStartTime()) {
                if (LOCK.tryLock()) {
                    try {
                        slice = slice.reset(startTime);
                        return slice;
                    } finally {
                        LOCK.unlock();
                    }
                } else {
                    Thread.yield();
                }
            }
            // 时钟回调问题，理论不应该出现这个问题，视为人为调整服务器时钟，返回的Slice不会被添加到时间轮中参与统计
            else {
                String error = "Current limiting counting failed due to unpredictable clock callback problem.";
                LOGGER.error(error);
                return new Slice(currentTimestamp, this.sliceInterval);
            }
        }
    }

    /**
     * 获取当前时间窗的总访问量
     * @return
     */
    public long getTotalCount(long currentTimestamp) {
        Slice[] slices = this.slices;
        long total = 0;
        long shouldStartTime = currentTimestamp - this.windowsInterval;
        for (Slice slice : slices) {
            if (slice!=null && slice.getStartTime()>=shouldStartTime) {
                total += slice.getTotalCounter().longValue();
            }
        }
        return total;
    }

    public long getPassCount(long currentTimestamp) {
        Slice[] slices = this.slices;
        long pass = 0;
        long shouldStartTime = currentTimestamp - this.windowsInterval;
        for (Slice slice : slices) {
            if (slice!=null && slice.getStartTime()>=shouldStartTime) {
                pass += slice.getPassCounter().longValue();
            }
        }
        return pass;
    }


    public long getQPS(long currentTimestamp) {
        long totalCount = this.getTotalCount(currentTimestamp);
        return (totalCount*1000) / this.windowsInterval;
    }

    private int calcCurrentTimeIdx(long currentTimestamp) {
        long vars = currentTimestamp / this.sliceInterval;
        return (int) (vars % this.slices.length);
    }

    private long calcCurrentTimeStartTime(long currentTimestamp) {
        return currentTimestamp - (currentTimestamp % this.sliceInterval);
    }

    private synchronized boolean setSliceToArray(Slice span, int idx) {
        Slice varSpan = this.slices[idx];
        if (varSpan != null) {
            if (varSpan.getStartTime() == span.getStartTime()) {
                return false;
            }
        }
        this.slices[idx] = span;
        return true;
    }


    public Slice[] slices() {
        return this.slices;
    }

}
